﻿//AcademicPlanner - course registration planning web app.
//Copyright (C) 2009  Boubacar Diallo

//This program is free software; you can redistribute it and/or
//modify it under the terms of the GNU General Public License
//as published by the Free Software Foundation; either version 2
//of the License, or (at your option) any later version.

//This program is distributed in the hope that it will be useful,
//but WITHOUT ANY WARRANTY; without even the implied warranty of
//MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
//GNU General Public License for more details.

//You should have received a copy of the GNU General Public License
//along with this program; if not, write to the Free Software
//Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.


using System;
using System.Text;
using System.Collections.Generic;
using AcademicPlanner.Domain.Models;
using AcademicPlanner.Framework.Libraries.Auth;
using AcademicPlanner.Framework.Helpers;

namespace AcademicPlanner.Domain.Components
{
	/// <summary>
	/// List of names of access control operations.
	/// </summary>
	/// <since>1.0</since>
	public enum ACOperationName
	{
		// Create Operations
		CREATE_PERSON,
		CREATE_ACCOUNT,
		CREATE_PROGRAM,
		CREATE_COURSE,
		CREATE_ACADEMICSCHEDULE,

		// Update Operations
		UPDATE_PERSON,
		UPDATE_ACADEMICPLAN,
		UPDATE_PROGRAM,
		UPDATE_COURSE,
		UPDATE_ACADEMICSCHEDULE,

		// Read Operations
		VIEW_PERSON,
		VIEW_ACADEMICPLAN,
		VIEW_PROGRAM,
		VIEW_COURSE,
		VIEW_ACADEMICSCHEDULE,

		// Delete Operations
		DELETE_PROGRAM,
		DELETE_COURSE,
		DELETE_ACADEMICSCHEDULE
	}

	/// <summary>
	/// List of names of access control roles.
	/// </summary>
	/// <since>1.0</since>
	public enum ACRoleName
	{
		GUEST,
		PLANNER
	}

	/// <summary>
	/// List of purposes of acess control object tokens.
	/// </summary>
	/// <since>1.0</since>
	public enum ACObjectTokenPurpose
	{
		AUTH
	}

	/// <summary>
	/// AuthMngr is the component of the AcademicPlanner application
	/// responsible for access control. AuthMngr uses 
	/// <see cref="T:AcademicPlanner.Framework.Libraries.Auth.DBorOpenIdBAuthenticator"/>
	/// for authentication and <see cref="T:AcademicPlanner.Framework.Libraries.Auth.RnRBAuthorizer"/>
	/// for authorization.
	/// </summary>
	/// <since>1.0</since>
	public class AuthMngr : Component
	{
		private IAuthenticator authn;

		private IAuthorizer authz;

		private AuthenticationResult authnResult;

		private IDictionary<string, bool?> permissionsCache;

		private bool inPrivilegedAuthorizationMode;

		private readonly string guestUserName = "sys_guest";

		private readonly string guestUserPassword = "";

		private readonly string objectTokenHashSalt = ";;objecttoken;;";

		/// <summary>
		/// Prefix for reserved system usernames.
		/// </summary>
		/// <since>1.0</since>
		public static readonly string SYS_USER_NAMES_PREFIX = "sys_";


		internal bool InPrivilegedAuthorizationMode { get { return inPrivilegedAuthorizationMode; } }


		private AuthMngr()
		{
			authn = new DBorOpenIdBAuthenticator(DomainMaster.DSAS.Connection.ConnectionString);
			authz = new RnRBAuthorizer(DomainMaster.DSAS.Connection.ConnectionString);
			permissionsCache = new Dictionary<string, bool?>();
		}

		private class InstanceHolder
		{
			internal static AuthMngr INSTANCE = new AuthMngr();
		}


		/// <summary>
		/// Gets the single AuthMngr instance.
		/// </summary>
		/// <returns>The single AuthMngr instance.</returns>
		/// <since>1.0</since>
		public static AuthMngr GetInstance()
		{
			return InstanceHolder.INSTANCE;
		}

		/// <summary>
		/// Returns whether the current user in the session is allowed
		/// to perform a given access controlled operation.
		/// </summary>
		/// <param name="pOperation">The access controlled operation.</param>
		/// <returns>Whether the access controlled operation is allowed.</returns>
		/// <seealso cref="M:IsUserAuthorized(ACOperationName, Model)"/>
		/// <since>1.0</since>
		internal bool IsUserAuthorized(ACOperationName pOperation)
		{
			return IsUserAuthorized(pOperation, null);
		}

		/// <summary>
		/// Returns whether the current user in the session is allowed
		/// to perform a given access controlled operation on a given <c>Model</c>.
		/// This method authenticates the session user first.
		/// </summary>
		/// <param name="pOperation">The access controlled operation.</param>
		/// <param name="pModel">
		/// The <c>Model</c> the operation is to be performed on; may be <c>null</c>.
		/// </param>
		/// <returns>Whether the access controlled operation is allowed.</returns>
		/// <seealso cref="M:AuthenticateUser"/>
		/// <since>1.0</since>
		internal bool IsUserAuthorized(ACOperationName pOperation, Model pModel)
		{
			bool rVal = false;
			bool ? vCachedVal = false;
			AuthorizationResult vRes;

			TryAuthenticateUser();
			if (authnResult.IsAuthentic)
			{
				if (!inPrivilegedAuthorizationMode)
				{
					vCachedVal = GetPermissionsCacheEntry(pOperation, pModel);
					if (vCachedVal == null)
					{
						var vUser = authnResult.UserIdentifier;
						var vOp = pOperation.ToString();

						if (pModel == null)
							vRes = authz.GetAuthorization(vUser, vOp);
						else
							vRes = authz.GetAuthorization(vUser, vOp, GetModelACObjectIdentifier(pModel));

						rVal = vRes.IsAuthorized;
						SetPermissionsCacheEntry(pOperation, pModel, rVal);
					}
					else
						rVal = (bool)vCachedVal;
				}
				else
					rVal = true;
			}

			return rVal;
		}

		private void SetPermissionsCacheEntry(ACOperationName pOperation, Model pModel, bool pValue)
		{
			var vKey = GetPermissionsCacheKey(pOperation, pModel);

			if (permissionsCache.ContainsKey(vKey))
				permissionsCache.Remove(vKey);

			permissionsCache.Add(vKey, pValue);
		}

		private bool? GetPermissionsCacheEntry(ACOperationName pOperation, Model pModel)
		{
			bool? rVal = null;

			permissionsCache.TryGetValue(GetPermissionsCacheKey(pOperation, pModel), out rVal);
			return rVal;
		}

		private string GetPermissionsCacheKey(ACOperationName pOperation, Model pModel)
		{
			var vFormat = "{0}--{1}--{2}";
			var vOp = pOperation.ToString();
			var vMod = (pModel != null) ? GetModelACObjectIdentifier(pModel) : "";

			return string.Format(vFormat, authnResult.UserIdentifier, vOp, vMod);
		}

		/// <summary>
		/// Authenticates the session user. If the session user's username is
		/// null or empty, he is assumed to be a guest.
		/// </summary>
		/// <seealso cref="M:AcademicPlanner.Framework.Helpers.SessionHelper.GetUserName"/>
		/// <seealso cref="M:AcademicPlanner.Framework.Helpers.SessionHelper.GetUserPassword"/>
		/// <since>1.0</since>
		public void AuthenticateUser()
		{
			var vUser = SessionHelper.GetUserName();
			var vPswd = SecurityHelper.Hash(SessionHelper.GetUserPassword(), User.PASSWORD_HASH_SALT);

			if (string.IsNullOrEmpty(vUser))
			{
				vUser = guestUserName;
				vPswd = guestUserPassword;
			}
			authnResult = authn.Authenticate(vUser, vPswd);
		}

		/// <summary>
		/// Checks whether a given user is authentic or not.
		/// </summary>
		/// <param name="pUser">The user identifier.</param>
		/// <param name="pPassword">The user password.</param>
		/// <returns>Whether the user is authentic or not.</returns>
		/// <since>1.0</since>
		public bool IsAuthenticUser(string pUser, string pPassword)
		{
			return authn.Authenticate(pUser, SecurityHelper.Hash(pPassword, User.PASSWORD_HASH_SALT))
							.IsAuthentic;
		}

		/// <summary>
		/// Checks whether the session user is authentic or not.
		/// </summary>
		/// <returns>Whether the session user is authentic or not.</returns>
		/// <since>1.0</since>
		public bool IsUserAuthentic()
		{
			return authnResult.IsAuthentic;
		}

		private void TryAuthenticateUser()
		{
			if (authnResult.UserIdentifier == null
				|| !authnResult.UserIdentifier.Equals(SessionHelper.GetUserName()))
				AuthenticateUser();
		}

		private string GetModelACObjectIdentifier(Model pModel)
		{
			return GetModelACObjectIdentifier(pModel.GetType(), pModel.EntityId);
		}

		private string GetModelACObjectIdentifier(Type pModelType, int pModelId)
		{
			return string.Format("{0}-{1}", pModelType.Name, pModelId);
		}

		/// <summary>
		/// Gets or creates an authentication token for a <c>Model</c>.
		/// </summary>
		/// <param name="pModel">The <c>Model</c>.</param>
		/// <returns>The authentication token.</returns>
		/// <exception cref="T:AcademicPlanner.Domain.CriticalDomainException">
		/// Thrown with error code <c>DomainErrorCode.C_ADD_ACOBJECT_TOKEN_FAILURE</c> 
		/// when an error occured while persisting the newly created token.
		/// </exception>
		/// <since>1.0</since>
		internal string GetAuthenticationToken(Model pModel)
		{
			var vObject = GetModelACObjectIdentifier(pModel);
			var vToken = ((RnRBAuthorizer)authz).GetNotExpiredObjectToken(vObject,
																			ACObjectTokenPurpose.AUTH.ToString());

			if (vToken == null)
			{
				vToken = SecurityHelper.Hash(DateTime.Now.ToLongDateString(),
											 new StringBuilder(vObject).Append(objectTokenHashSalt)
																		.Append(ACObjectTokenPurpose.AUTH)
																		.ToString());
				if (!((RnRBAuthorizer)authz).AddObjectToken(vObject, ACObjectTokenPurpose.AUTH.ToString(), vToken))
					throw new CriticalDomainException(DomainErrorCode.C_ADD_ACOBJECT_TOKEN_FAILURE);
			}

			return vToken;
		}

		/// <summary>
		/// Returns whether an authentication token is valid for a given <c>Model</c>.
		/// </summary>
		/// <param name="pToken">The authentication token.</param>
		/// <param name="pModelType">The <c>System.Type</c> of the <c>Model</c>.</param>
		/// <param name="pModelId">The <c>Model</c> identifier.</param>
		/// <returns>Whether the authentication token is valid or not.</returns>
		/// <since>1.0</since>
		internal bool IsAuthenticationTokenValid(string pToken, Type pModelType, int pModelId)
		{
			var vObject = GetModelACObjectIdentifier(pModelType, pModelId);
			var vToken = ((RnRBAuthorizer)authz).GetNotExpiredObjectToken(vObject,
																				ACObjectTokenPurpose.AUTH.ToString());
			return (vToken != null && vToken.Equals(pToken));
		}

		/// <summary>
		/// Returns whether an authentication token is valid for a given <c>Model</c>.
		/// </summary>
		/// <param name="pToken">The authentication token.</param>
		/// <param name="pModel">The <c>Model</c>.</param>
		/// <returns>Whether the authentication token is valid or not.</returns>
		/// <since>1.0</since>
		internal bool IsAuthenticationTokenValid(string pToken, Model pModel)
		{
			return IsAuthenticationTokenValid(pToken, pModel.GetType(), pModel.EntityId);
		}

		/// <summary>
		/// Discards the authentication token of a <c>Model</c> and renders it
		/// invalid for future use.
		/// </summary>
		/// <param name="pModel">The <c>Model</c>.</param>
		/// <since>1.0</since>
		internal void DiscardAuthenticationToken(Model pModel)
		{
			var vObject = GetModelACObjectIdentifier(pModel);
			((RnRBAuthorizer)authz).ExpireObjectToken(vObject, ACObjectTokenPurpose.AUTH.ToString());
		}

		/// <summary>
		/// Registers a <c>Model</c> into the access control sub system.
		/// </summary>
		/// <param name="pModel">The <c>Model</c> to register.</param>
		/// <param name="pUser">The <c>User</c> "owning" the <c>Model</c>.</param>
		/// <since>1.0</since>
		internal void RegisterIntoACSystem(Model pModel, User pUser)
		{
			var vObject = GetModelACObjectIdentifier(pModel);

			if (!((RnRBAuthorizer)authz).AddObject(vObject, pUser.EntityId))
				throw new CriticalDomainException(DomainErrorCode.C_ADD_ACOBJECT_FAILURE);
		}

		internal void AssignUserRole(User pUser, ACRoleName pRole)
		{
			if (!((RnRBAuthorizer)authz).AssignRoleToUser(pRole.ToString(), pUser.EntityId))
				throw new CriticalDomainException(DomainErrorCode.C_ASSIGN_ROLE_TO_USER_FAILURE);
		}

		/// <summary>
		/// Returns whether a username is allowed as such for a <c>User</c>.
		/// <c>User</c>s usernames are not allowed to start with <see cref="P:SYS_USER_NAMES_PREFIX"/>.
		/// </summary>
		/// <param name="pUserName">The username to check.</param>
		/// <returns>Whether the username is allowed.</returns>
		/// <since>1.0</since>
		public bool IsUserNameAllowed(string pUserName)
		{
			return !pUserName.StartsWith(SYS_USER_NAMES_PREFIX);
		}

		internal void EnterPrivelegedAuthorizationMode()
		{
			inPrivilegedAuthorizationMode = true;
		}

		internal void ExitPrivelegedAuthorizationMode()
		{
			inPrivilegedAuthorizationMode = false;
		}
	}
}
